สำรวจการสื่อสารข้ามต้นทางที่ปลอดภัยโดยใช้ PostMessage API เรียนรู้เกี่ยวกับความสามารถ ความเสี่ยงด้านความปลอดภัย และแนวทางปฏิบัติที่ดีที่สุดเพื่อลดช่องโหว่ในเว็บแอปพลิเคชัน
การสื่อสารข้ามต้นทาง (Cross-Origin): รูปแบบความปลอดภัยด้วย PostMessage API
ในเว็บยุคใหม่ แอปพลิเคชันมักจำเป็นต้องโต้ตอบกับทรัพยากรจากต้นทาง (origin) ที่แตกต่างกัน Same-Origin Policy (SOP) เป็นกลไกความปลอดภัยที่สำคัญซึ่งจำกัดไม่ให้สคริปต์เข้าถึงทรัพยากรจากต้นทางอื่น อย่างไรก็ตาม มีสถานการณ์ที่ถูกต้องตามกฎหมายซึ่งจำเป็นต้องมีการสื่อสารข้ามต้นทาง postMessage API เป็นกลไกที่ควบคุมได้เพื่อให้บรรลุเป้าหมายนี้ แต่สิ่งสำคัญคือต้องเข้าใจความเสี่ยงด้านความปลอดภัยที่อาจเกิดขึ้นและนำรูปแบบความปลอดภัยที่เหมาะสมมาใช้
ทำความเข้าใจ Same-Origin Policy (SOP)
Same-Origin Policy เป็นแนวคิดพื้นฐานด้านความปลอดภัยในเว็บเบราว์เซอร์ โดยจะจำกัดไม่ให้เว็บเพจส่งคำขอไปยังโดเมนที่แตกต่างจากโดเมนที่ให้บริการเว็บเพจนั้น ต้นทาง (origin) ถูกกำหนดโดย scheme (โปรโตคอล), host (โดเมน) และ port หากส่วนใดส่วนหนึ่งเหล่านี้แตกต่างกัน จะถือว่าเป็นคนละต้นทาง ตัวอย่างเช่น:
https://example.comhttps://www.example.comhttp://example.comhttps://example.com:8080
ทั้งหมดนี้คือต้นทางที่แตกต่างกัน และ SOP จะจำกัดการเข้าถึงโดยตรงของสคริปต์ระหว่างกัน
แนะนำ PostMessage API
postMessage API เป็นกลไกที่ปลอดภัยและควบคุมได้สำหรับการสื่อสารข้ามต้นทาง ช่วยให้สคริปต์สามารถส่งข้อความไปยังหน้าต่างอื่น ๆ (เช่น iframes, หน้าต่างใหม่ หรือแท็บ) โดยไม่คำนึงถึงต้นทางของมัน จากนั้นหน้าต่างผู้รับสามารถรอรับฟังข้อความเหล่านี้และประมวลผลตามนั้นได้
ไวยากรณ์พื้นฐานสำหรับการส่งข้อความคือ:
otherWindow.postMessage(message, targetOrigin);
otherWindow: การอ้างอิงถึงหน้าต่างเป้าหมาย (เช่นwindow.parent,iframe.contentWindowหรืออ็อบเจกต์หน้าต่างที่ได้จากwindow.open)message: ข้อมูลที่คุณต้องการส่ง ซึ่งอาจเป็นอ็อบเจกต์ JavaScript ใด ๆ ที่สามารถ serialize ได้ (เช่น สตริง, ตัวเลข, อ็อบเจกต์, อาร์เรย์)targetOrigin: ระบุต้นทางที่คุณต้องการส่งข้อความไป นี่เป็นพารามิเตอร์ความปลอดภัยที่สำคัญอย่างยิ่ง
ในฝั่งผู้รับ คุณต้องรอรับฟังอีเวนต์ message:
window.addEventListener('message', function(event) {
// ...
});
อ็อบเจกต์ event ประกอบด้วยคุณสมบัติดังต่อไปนี้:
event.data: ข้อความที่ส่งมาจากหน้าต่างอื่นevent.origin: ต้นทางของหน้าต่างที่ส่งข้อความevent.source: การอ้างอิงถึงหน้าต่างที่ส่งข้อความ
ความเสี่ยงและช่องโหว่ด้านความปลอดภัย
แม้ว่า postMessage จะเป็นช่องทางในการหลีกเลี่ยงข้อจำกัดของ SOP แต่มันก็นำมาซึ่งความเสี่ยงด้านความปลอดภัยที่อาจเกิดขึ้นได้หากไม่ได้ใช้งานอย่างระมัดระวัง นี่คือช่องโหว่ที่พบบ่อยบางส่วน:
1. Target Origin ไม่ตรงกัน
การไม่ตรวจสอบคุณสมบัติ event.origin เป็นช่องโหว่ที่ร้ายแรง หากผู้รับเชื่อถือข้อความโดยไม่ตรวจสอบ เว็บไซต์ใด ๆ ก็สามารถส่งข้อมูลที่เป็นอันตรายเข้ามาได้ ควรตรวจสอบเสมอว่า event.origin ตรงกับต้นทางที่คาดหวังก่อนที่จะประมวลผลข้อความ
ตัวอย่าง (โค้ดที่มีช่องโหว่):
window.addEventListener('message', function(event) {
// อย่าทำแบบนี้!
processMessage(event.data);
});
ตัวอย่าง (โค้ดที่ปลอดภัย):
window.addEventListener('message', function(event) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Received message from untrusted origin:', event.origin);
return;
}
processMessage(event.data);
});
2. การแทรกข้อมูล (Data Injection)
การปฏิบัติต่อข้อมูลที่ได้รับ (event.data) เหมือนเป็นโค้ดที่สั่งการได้ หรือการแทรกข้อมูลลงใน DOM โดยตรงอาจนำไปสู่ช่องโหว่ Cross-Site Scripting (XSS) ควรทำความสะอาด (sanitize) และตรวจสอบความถูกต้องของข้อมูลที่ได้รับเสมอก่อนนำไปใช้
ตัวอย่าง (โค้ดที่มีช่องโหว่):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
document.body.innerHTML = event.data; // อย่าทำแบบนี้!
}
});
ตัวอย่าง (โค้ดที่ปลอดภัย):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
const sanitizedData = sanitize(event.data); // เขียนฟังก์ชันทำความสะอาดข้อมูลที่เหมาะสม
document.getElementById('message-container').textContent = sanitizedData;
}
});
function sanitize(data) {
// เขียนตรรกะการทำความสะอาดข้อมูลที่แข็งแกร่งที่นี่
// ตัวอย่างเช่น ใช้ DOMPurify หรือไลบรารีที่คล้ายกัน
return DOMPurify.sanitize(data);
}
3. การโจมตีแบบ Man-in-the-Middle (MITM)
หากการสื่อสารเกิดขึ้นผ่านช่องทางที่ไม่ปลอดภัย (HTTP) ผู้โจมตีแบบ MITM สามารถดักจับและแก้ไขข้อความได้ ควรใช้ HTTPS เสมอเพื่อการสื่อสารที่ปลอดภัย
4. การโจมตีแบบ Cross-Site Request Forgery (CSRF)
หากผู้รับดำเนินการตามข้อความที่ได้รับโดยไม่มีการตรวจสอบที่เหมาะสม ผู้โจมตีอาจสามารถปลอมแปลงข้อความเพื่อหลอกให้ผู้รับดำเนินการที่ไม่พึงประสงค์ได้ ควรใช้กลไกป้องกัน CSRF เช่น การรวมโทเค็นลับไว้ในข้อความและตรวจสอบในฝั่งผู้รับ
5. การใช้ Wildcard ใน targetOrigin
การตั้งค่า targetOrigin เป็น * จะอนุญาตให้ทุกต้นทางรับข้อความได้ ซึ่งควรหลีกเลี่ยงเว้นแต่จะจำเป็นจริงๆ เนื่องจากมันทำลายวัตถุประสงค์ของความปลอดภัยตามต้นทาง หากจำเป็นต้องใช้ * ต้องแน่ใจว่าได้ใช้มาตรการความปลอดภัยที่เข้มแข็งอื่น ๆ ร่วมด้วย เช่น รหัสยืนยันข้อความ (MACs)
ตัวอย่าง (สิ่งที่ควรหลีกเลี่ยง):
otherWindow.postMessage(message, '*'); // หลีกเลี่ยงการใช้ '*' หากไม่จำเป็นจริงๆ
รูปแบบความปลอดภัยและแนวทางปฏิบัติที่ดีที่สุด
เพื่อลดความเสี่ยงที่เกี่ยวข้องกับ postMessage ให้ปฏิบัติตามรูปแบบความปลอดภัยและแนวทางปฏิบัติที่ดีที่สุดเหล่านี้:
1. การตรวจสอบต้นทางอย่างเข้มงวด
ตรวจสอบคุณสมบัติ event.origin ในฝั่งผู้รับเสมอ เปรียบเทียบกับรายการต้นทางที่เชื่อถือได้ที่กำหนดไว้ล่วงหน้า ใช้การเปรียบเทียบแบบเข้มงวด (===)
2. การทำความสะอาดและตรวจสอบความถูกต้องของข้อมูล
ทำความสะอาดและตรวจสอบข้อมูลทั้งหมดที่ได้รับผ่าน postMessage ก่อนนำไปใช้ ใช้เทคนิคการทำความสะอาดที่เหมาะสมขึ้นอยู่กับวิธีการใช้ข้อมูล (เช่น HTML escaping, URL encoding, การตรวจสอบอินพุต) ใช้ไลบรารีอย่าง DOMPurify สำหรับการทำความสะอาด HTML
3. รหัสยืนยันข้อความ (Message Authentication Codes - MACs)
รวมรหัสยืนยันข้อความ (MAC) ไว้ในข้อความเพื่อรับรองความสมบูรณ์และความถูกต้องของข้อมูล ผู้ส่งจะคำนวณ MAC โดยใช้กุญแจลับร่วม (shared secret key) และรวมไว้ในข้อความ ผู้รับจะคำนวณ MAC ใหม่โดยใช้กุญแจลับร่วมเดียวกันและเปรียบเทียบกับ MAC ที่ได้รับ หากตรงกัน แสดงว่าข้อความเป็นของแท้และไม่ถูกแก้ไข
ตัวอย่าง (การใช้ HMAC-SHA256):
// ฝั่งผู้ส่ง
async function sendMessage(message, targetOrigin, sharedSecret) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: message,
signature: signatureHex
};
otherWindow.postMessage(securedMessage, targetOrigin);
}
// ฝั่งผู้รับ
async function receiveMessage(event, sharedSecret) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Received message from untrusted origin:', event.origin);
return;
}
const securedMessage = event.data;
const message = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Message is authentic!');
processMessage(message); // ดำเนินการประมวลผลข้อความต่อไป
} else {
console.error('Message signature verification failed!');
}
}
สำคัญ: กุญแจลับร่วมต้องถูกสร้างและจัดเก็บอย่างปลอดภัย หลีกเลี่ยงการ hardcode กุญแจในโค้ด
4. การใช้ Nonce และ Timestamps
เพื่อป้องกันการโจมตีแบบ Replay Attack ให้รวม nonce (number used once) ที่ไม่ซ้ำกันและ timestamp ไว้ในข้อความ จากนั้นผู้รับสามารถตรวจสอบได้ว่า nonce นี้ไม่เคยถูกใช้มาก่อนและ timestamp อยู่ในช่วงเวลาที่ยอมรับได้ ซึ่งจะช่วยลดความเสี่ยงที่ผู้โจมตีจะส่งข้อความที่เคยดักจับไว้ซ้ำ
5. หลักการให้สิทธิ์น้อยที่สุด (Principle of Least Privilege)
ให้สิทธิ์แก่หน้าต่างอื่นเท่าที่จำเป็นเท่านั้น ตัวอย่างเช่น หากหน้าต่างอื่นต้องการเพียงอ่านข้อมูล ก็อย่าอนุญาตให้เขียนข้อมูล ออกแบบโปรโตคอลการสื่อสารของคุณโดยคำนึงถึงหลักการให้สิทธิ์น้อยที่สุด
6. นโยบายความปลอดภัยของเนื้อหา (Content Security Policy - CSP)
ใช้นโยบายความปลอดภัยของเนื้อหา (CSP) เพื่อจำกัดแหล่งที่มาที่สามารถโหลดสคริปต์ได้และการกระทำที่สคริปต์สามารถทำได้ ซึ่งสามารถช่วยลดผลกระทบของช่องโหว่ XSS ที่อาจเกิดขึ้นจากการจัดการข้อมูล postMessage ที่ไม่เหมาะสม
7. การตรวจสอบความถูกต้องของอินพุต
ตรวจสอบโครงสร้างและรูปแบบของข้อมูลที่ได้รับเสมอ กำหนดรูปแบบข้อความที่ชัดเจนและตรวจสอบให้แน่ใจว่าข้อมูลที่ได้รับเป็นไปตามรูปแบบนี้ ซึ่งจะช่วยป้องกันพฤติกรรมที่ไม่คาดคิดและช่องโหว่ได้
8. การทำ Serialization ข้อมูลอย่างปลอดภัย
ใช้รูปแบบการทำ serialization ข้อมูลที่ปลอดภัย เช่น JSON เพื่อ serialize และ deserialize ข้อความ หลีกเลี่ยงการใช้รูปแบบที่อนุญาตให้มีการรันโค้ดได้ เช่น eval() หรือ Function()
9. จำกัดขนาดของข้อความ
จำกัดขนาดของข้อความที่ส่งผ่าน postMessage ข้อความขนาดใหญ่อาจใช้ทรัพยากรมากเกินไปและอาจนำไปสู่การโจมตีแบบ Denial-of-Service ได้
10. การตรวจสอบความปลอดภัยอย่างสม่ำเสมอ
ดำเนินการตรวจสอบความปลอดภัยของโค้ดของคุณอย่างสม่ำเสมอเพื่อระบุและแก้ไขช่องโหว่ที่อาจเกิดขึ้น ให้ความสำคัญกับการใช้งาน postMessage และตรวจสอบให้แน่ใจว่าได้ปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดด้านความปลอดภัยทั้งหมด
ตัวอย่างสถานการณ์: การสื่อสารที่ปลอดภัยระหว่าง Iframe และ Parent
พิจารณาสถานการณ์ที่ iframe ซึ่งโฮสต์บน https://iframe.example.com ต้องการสื่อสารกับหน้า parent ที่โฮสต์บน https://parent.example.com โดย iframe ต้องการส่งข้อมูลผู้ใช้ไปยังหน้า parent เพื่อประมวลผล
Iframe (https://iframe.example.com):
// สร้าง shared secret key (แทนที่ด้วยวิธีการสร้างคีย์ที่ปลอดภัย)
const sharedSecret = 'YOUR_SECURE_SHARED_SECRET';
// รับข้อมูลผู้ใช้
const userData = {
name: 'John Doe',
email: 'john.doe@example.com'
};
// ส่งข้อมูลผู้ใช้ไปยังหน้า parent
async function sendUserData(userData) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: userData,
signature: signatureHex
};
parent.postMessage(securedMessage, 'https://parent.example.com');
}
sendUserData(userData);
Parent Page (https://parent.example.com):
// Shared secret key (ต้องตรงกับคีย์ของ iframe)
const sharedSecret = 'YOUR_SECURE_SHARED_SECRET';
window.addEventListener('message', async function(event) {
if (event.origin !== 'https://iframe.example.com') {
console.warn('Received message from untrusted origin:', event.origin);
return;
}
const securedMessage = event.data;
const userData = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Message is authentic!');
// ประมวลผลข้อมูลผู้ใช้
console.log('User data:', userData);
} else {
console.error('Message signature verification failed!');
}
});
ข้อควรทราบ:
- แทนที่
YOUR_SECURE_SHARED_SECRETด้วยกุญแจลับร่วมที่สร้างขึ้นอย่างปลอดภัย - กุญแจลับร่วมต้องเป็นค่าเดียวกันทั้งใน iframe และหน้า parent
- ตัวอย่างนี้ใช้ HMAC-SHA256 สำหรับการยืนยันข้อความ
บทสรุป
postMessage API เป็นเครื่องมือที่มีประสิทธิภาพในการเปิดใช้งานการสื่อสารข้ามต้นทางในเว็บแอปพลิเคชัน อย่างไรก็ตาม สิ่งสำคัญคือต้องเข้าใจความเสี่ยงด้านความปลอดภัยที่อาจเกิดขึ้นและนำรูปแบบความปลอดภัยที่เหมาะสมมาใช้เพื่อลดความเสี่ยงเหล่านี้ โดยการปฏิบัติตามรูปแบบความปลอดภัยและแนวทางปฏิบัติที่ดีที่สุดที่ระบุไว้ในคู่มือนี้ คุณสามารถใช้ postMessage ได้อย่างปลอดภัยเพื่อสร้างเว็บแอปพลิเคชันที่แข็งแกร่งและปลอดภัย
อย่าลืมให้ความสำคัญกับความปลอดภัยเสมอและติดตามข่าวสารล่าสุดเกี่ยวกับแนวทางปฏิบัติที่ดีที่สุดด้านความปลอดภัยสำหรับการพัฒนาเว็บ ตรวจสอบโค้ดและการกำหนดค่าความปลอดภัยของคุณอย่างสม่ำเสมอเพื่อให้แน่ใจว่าแอปพลิเคชันของคุณได้รับการป้องกันจากช่องโหว่ที่อาจเกิดขึ้น